atspi: Make text change notification work
authorMatthias Clasen <mclasen@redhat.com>
Tue, 13 Oct 2020 04:50:19 +0000 (00:50 -0400)
committerMatthias Clasen <mclasen@redhat.com>
Tue, 13 Oct 2020 13:44:04 +0000 (09:44 -0400)
Make text change notification work for editables, by connecting
to the ::insert-text and ::delete-text signals on the wrapped
GtkText widget, and for GtkTextView by connecting to the
corresponding GtkTextBuffer signals.

This code is more or less directly copied from GtkTextViewAccessible
and GtkEntryAccessible in GTK 3.

gtk/a11y/gtkatspicontext.c
gtk/a11y/gtkatspitext.c
gtk/a11y/gtkatspitextprivate.h

index 92562e4a04b8ff1eeef1835ce2b6a7112fa82d17..9a5f58fd47cd2306738a67340bcbbd1f2956e219 100644 (file)
@@ -39,6 +39,7 @@
 
 #include "gtkdebug.h"
 #include "gtkeditable.h"
+#include "gtkentryprivate.h"
 #include "gtkroot.h"
 #include "gtktextview.h"
 #include "gtkwindow.h"
@@ -670,6 +671,38 @@ gtk_at_spi_context_unregister_object (GtkAtSpiContext *self)
     }
 }
 
+static void
+emit_text_changed (GtkAtSpiContext *self,
+                   const char      *kind,
+                   int              start,
+                   int              end,
+                   const char      *text)
+{
+  g_dbus_connection_emit_signal (self->connection,
+                                 NULL,
+                                 self->context_path,
+                                 "org.a11y.atspi.Event.Object",
+                                 "TextChanged",
+                                 g_variant_new ("(siiva{sv})",
+                                                kind, start, end, g_variant_new_string (text), NULL),
+                                 NULL);
+}
+
+static void
+emit_selection_changed (GtkAtSpiContext *self,
+                        const char      *kind,
+                        int              cursor_position)
+{
+  g_dbus_connection_emit_signal (self->connection,
+                                 NULL,
+                                 self->context_path,
+                                 "org.a11y.atspi.Event.Object",
+                                 "TextChanged",
+                                 g_variant_new ("(siiva{sv})",
+                                                kind, cursor_position, 0, g_variant_new_string (""), NULL),
+                                 NULL);
+}
+
 static void
 emit_state_changed (GtkAtSpiContext *self,
                     const char      *name,
@@ -874,12 +907,45 @@ gtk_at_spi_context_state_change (GtkATContext                *ctx,
     }
 }
 
+static void
+insert_text_cb (GtkEditable     *editable,
+                char            *new_text,
+                int              new_text_length,
+                int             *position,
+                GtkAtSpiContext *self)
+{
+  int length;
+
+  if (new_text_length == 0)
+    return;
+
+  length = g_utf8_strlen (new_text, new_text_length);
+  emit_text_changed (self, "insert", *position - length, length, new_text);
+}
+
+static void
+delete_text_cb (GtkEditable     *editable,
+                int              start,
+                int              end,
+                GtkAtSpiContext *self)
+{
+  char *text;
+
+  if (start == end)
+    return;
+
+  text = gtk_editable_get_chars (editable, start, end);
+  emit_text_changed (self, "delete", start, end - start, text);
+}
+
 static void
 gtk_at_spi_context_dispose (GObject *gobject)
 {
   GtkAtSpiContext *self = GTK_AT_SPI_CONTEXT (gobject);
+  GtkAccessible *accessible = gtk_at_context_get_accessible (GTK_AT_CONTEXT (self));
 
   gtk_at_spi_context_unregister_object (self);
+  gtk_atspi_disconnect_text_signals (GTK_WIDGET (accessible));
 
   G_OBJECT_CLASS (gtk_at_spi_context_parent_class)->dispose (gobject);
 }
@@ -1002,6 +1068,11 @@ gtk_at_spi_context_constructed (GObject *gobject)
   g_free (base_path);
   g_free (uuid);
 
+  GtkAccessible *accessible = gtk_at_context_get_accessible (GTK_AT_CONTEXT (self));
+  gtk_atspi_connect_text_signals (GTK_WIDGET (accessible),
+                                  emit_text_changed,
+                                  emit_selection_changed,
+                                  self);
   gtk_at_spi_context_register_object (self);
 
   G_OBJECT_CLASS (gtk_at_spi_context_parent_class)->constructed (gobject);
index bf8bdb79c4e74b7e1610b680d41bfc2350b7ea97..9a7b37b5a3b3503854a738c05d2e9518139038f6 100644 (file)
@@ -404,6 +404,20 @@ static const GDBusInterfaceVTable label_vtable = {
   NULL,
 };
 
+static GtkText *
+gtk_editable_get_text_widget (GtkWidget *widget)
+{
+  if (GTK_IS_ENTRY (widget))
+    return gtk_entry_get_text_widget (GTK_ENTRY (widget));
+  else if (GTK_IS_SEARCH_ENTRY (widget))
+    return gtk_search_entry_get_text_widget (GTK_SEARCH_ENTRY (widget));
+  else if (GTK_IS_PASSWORD_ENTRY (widget))
+    return gtk_password_entry_get_text_widget (GTK_PASSWORD_ENTRY (widget));
+  else if (GTK_IS_SPIN_BUTTON (widget))
+    return gtk_spin_button_get_text_widget (GTK_SPIN_BUTTON (widget));
+
+  return NULL;
+}
 
 static void
 entry_handle_method (GDBusConnection       *connection,
@@ -418,16 +432,7 @@ entry_handle_method (GDBusConnection       *connection,
   GtkATContext *self = user_data;
   GtkAccessible *accessible = gtk_at_context_get_accessible (self);
   GtkWidget *widget = GTK_WIDGET (accessible);
-  GtkText *text_widget;
-
-  if (GTK_IS_ENTRY (widget))
-    text_widget = gtk_entry_get_text_widget (GTK_ENTRY (widget));
-  else if (GTK_IS_SEARCH_ENTRY (widget))
-    text_widget = gtk_search_entry_get_text_widget (GTK_SEARCH_ENTRY (widget));
-  else if (GTK_IS_PASSWORD_ENTRY (widget))
-    text_widget = gtk_password_entry_get_text_widget (GTK_PASSWORD_ENTRY (widget));
-  else if (GTK_IS_SPIN_BUTTON (widget))
-    text_widget = gtk_spin_button_get_text_widget (GTK_SPIN_BUTTON (widget));
+  GtkText *text_widget = gtk_editable_get_text_widget (widget);
 
   if (g_strcmp0 (method_name, "GetCaretOffset") == 0)
     {
@@ -1165,3 +1170,281 @@ gtk_atspi_get_text_vtable (GtkWidget *widget)
 
   return NULL;
 }
+
+typedef struct {
+  void (* text_changed)      (gpointer    data,
+                              const char *kind,
+                              int         start,
+                              int         end,
+                              const char *text);
+  void (* selection_changed) (gpointer    data,
+                              const char *kind,
+                              int         cursor_position);
+
+  gpointer data;
+  GtkTextBuffer *buffer;
+  int cursor_position;
+  int selection_bound;
+} TextChanged;
+
+static void
+insert_text_cb (GtkEditable     *editable,
+                char            *new_text,
+                int              new_text_length,
+                int             *position,
+                TextChanged     *changed)
+{
+  int length;
+
+  if (new_text_length == 0)
+    return;
+
+  length = g_utf8_strlen (new_text, new_text_length);
+  changed->text_changed (changed->data, "insert", *position - length, length, new_text);
+}
+
+static void
+delete_text_cb (GtkEditable     *editable,
+                int              start,
+                int              end,
+                TextChanged     *changed)
+{
+  char *text;
+
+  if (start == end)
+    return;
+
+  text = gtk_editable_get_chars (editable, start, end);
+  changed->text_changed (changed->data, "delete", start, end - start, text);
+  g_free (text);
+}
+
+static void
+update_selection (TextChanged *changed,
+                  int          cursor_position,
+                  int          selection_bound)
+{
+  gboolean caret_moved, bound_moved;
+
+  caret_moved = cursor_position != changed->cursor_position;
+  bound_moved = selection_bound != changed->selection_bound;
+
+  if (!caret_moved && !bound_moved)
+    return;
+
+  changed->cursor_position = cursor_position;
+  changed->selection_bound = selection_bound;
+
+  if (caret_moved)
+    changed->selection_changed (changed->data, "text-caret-moved", changed->cursor_position);
+
+  if (caret_moved || bound_moved)
+    changed->selection_changed (changed->data, "text-selection-changed", 0);
+}
+
+static void
+notify_cb (GObject     *object,
+           GParamSpec  *pspec,
+           TextChanged *changed)
+{
+  if (g_strcmp0 (pspec->name, "cursor-position") == 0 ||
+      g_strcmp0 (pspec->name, "selection-bound") == 0)
+    {
+      int cursor_position, selection_bound;
+
+      gtk_editable_get_selection_bounds (GTK_EDITABLE (object), &cursor_position, &selection_bound);
+      update_selection (changed, cursor_position, selection_bound);
+    }
+}
+
+static void
+update_cursor (GtkTextBuffer *buffer,
+               TextChanged   *changed)
+{
+  GtkTextIter iter;
+  int cursor_position, selection_bound;
+
+  gtk_text_buffer_get_iter_at_mark (buffer, &iter, gtk_text_buffer_get_insert (buffer));
+  cursor_position = gtk_text_iter_get_offset (&iter);
+
+  gtk_text_buffer_get_iter_at_mark (buffer, &iter, gtk_text_buffer_get_selection_bound (buffer));
+
+  selection_bound = gtk_text_iter_get_offset (&iter);
+
+  update_selection (changed, cursor_position, selection_bound);
+}
+
+static void
+insert_range_cb (GtkTextBuffer *buffer,
+                 GtkTextIter   *iter,
+                 char          *text,
+                 int            len,
+                 TextChanged   *changed)
+{
+  int position;
+  int length;
+
+  position = gtk_text_iter_get_offset (iter);
+  length = g_utf8_strlen (text, len);
+
+  changed->text_changed (changed->data, "insert", position - length, length, text);
+
+  update_cursor (buffer, changed);
+}
+
+static void
+delete_range_cb (GtkTextBuffer *buffer,
+                 GtkTextIter   *start,
+                 GtkTextIter   *end,
+                 TextChanged   *changed)
+{
+  int offset, length;
+  char *text;
+
+  text = gtk_text_buffer_get_slice (buffer, start, end, FALSE);
+
+  offset = gtk_text_iter_get_offset (start);
+  length = gtk_text_iter_get_offset (end) - offset;
+
+  changed->text_changed (changed->data, "delete", offset, length, text);
+
+  g_free (text);
+}
+
+static void
+delete_range_after_cb (GtkTextBuffer *buffer,
+                       GtkTextIter   *start,
+                       GtkTextIter   *end,
+                       TextChanged   *changed)
+{
+  update_cursor (buffer, changed);
+}
+
+static void
+mark_set_cb (GtkTextBuffer *buffer,
+             GtkTextIter   *location,
+             GtkTextMark   *mark,
+             TextChanged   *changed)
+{
+  if (mark == gtk_text_buffer_get_insert (buffer) ||
+      mark == gtk_text_buffer_get_selection_bound (buffer))
+    update_cursor (buffer, changed);
+}
+
+static void
+buffer_changed (GtkWidget   *widget,
+                GParamSpec  *pspec,
+                TextChanged *changed)
+{
+  GtkTextBuffer *buffer;
+  GtkTextIter start, end;
+  char *text;
+
+  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (widget));
+
+  if (changed->buffer)
+    {
+      g_signal_handlers_disconnect_by_func (changed->buffer, insert_range_cb, changed);
+      g_signal_handlers_disconnect_by_func (changed->buffer, delete_range_cb, changed);
+      g_signal_handlers_disconnect_by_func (changed->buffer, delete_range_after_cb, changed);
+      g_signal_handlers_disconnect_by_func (changed->buffer, mark_set_cb, changed);
+
+      gtk_text_buffer_get_bounds (changed->buffer, &start, &end);
+      text = gtk_text_buffer_get_slice (changed->buffer, &start, &end, FALSE);
+      changed->text_changed (changed->data, "delete", 0, gtk_text_buffer_get_char_count (changed->buffer), text);
+      g_free (text);
+
+      update_selection (changed, 0, 0);
+
+      g_clear_object (&changed->buffer);
+    }
+
+  changed->buffer = buffer;
+
+  if (changed->buffer)
+    {
+      g_object_ref (changed->buffer);
+      g_signal_connect (changed->buffer, "insert-text", G_CALLBACK (insert_range_cb), changed);
+      g_signal_connect (changed->buffer, "delete-range", G_CALLBACK (delete_range_cb), changed);
+      g_signal_connect_after (changed->buffer, "delete-range", G_CALLBACK (delete_range_after_cb), changed);
+      g_signal_connect_after (changed->buffer, "mark-set", G_CALLBACK (mark_set_cb), changed);
+
+      gtk_text_buffer_get_bounds (changed->buffer, &start, &end);
+      text = gtk_text_buffer_get_slice (changed->buffer, &start, &end, FALSE);
+      changed->text_changed (changed->data, "insert", 0, gtk_text_buffer_get_char_count (changed->buffer), text);
+      g_free (text);
+
+      update_cursor (changed->buffer, changed);
+    }
+}
+
+void
+gtk_atspi_connect_text_signals (GtkWidget *widget,
+                                GtkAtspiTextChangedCallback text_changed,
+                                GtkAtspiSelectionChangedCallback selection_changed,
+                                gpointer   data)
+{
+  TextChanged *changed;
+
+  changed = g_new0 (TextChanged, 1);
+  changed->text_changed = text_changed;
+  changed->selection_changed = selection_changed;
+  changed->data = data;
+
+  g_object_set_data_full (G_OBJECT (widget), "accessible-text-data", changed, g_free);
+
+  if (GTK_IS_EDITABLE (widget))
+    {
+      GtkText *text = gtk_editable_get_text_widget (widget);
+
+      if (text)
+        {
+          g_signal_connect_after (text, "insert-text", G_CALLBACK (insert_text_cb), changed);
+          g_signal_connect (text, "delete-text", G_CALLBACK (delete_text_cb), changed);
+          g_signal_connect (text, "notify", G_CALLBACK (notify_cb), changed);
+
+          gtk_editable_get_selection_bounds (GTK_EDITABLE (text), &changed->cursor_position, &changed->selection_bound);
+        }
+    }
+  else if (GTK_IS_TEXT_VIEW (widget))
+    {
+      g_signal_connect (widget, "notify::buffer", G_CALLBACK (buffer_changed), changed);
+      buffer_changed (widget, NULL, changed);
+    }
+}
+
+void
+gtk_atspi_disconnect_text_signals (GtkWidget *widget)
+{
+  TextChanged *changed;
+
+  changed = g_object_get_data (G_OBJECT (widget), "accessible-text-data");
+
+  g_assert (changed != NULL);
+
+  if (GTK_IS_EDITABLE (widget))
+    {
+      GtkText *text = gtk_editable_get_text_widget (widget);
+
+      if (text)
+        {
+          g_signal_handlers_disconnect_by_func (text, insert_text_cb, changed);
+          g_signal_handlers_disconnect_by_func (text, delete_text_cb, changed);
+          g_signal_handlers_disconnect_by_func (text, notify_cb, changed);
+        }
+    }
+  else if (GTK_IS_TEXT_VIEW (widget))
+    {
+      g_signal_handlers_disconnect_by_func (widget, buffer_changed, changed);
+      if (changed->buffer)
+        {
+          g_signal_handlers_disconnect_by_func (changed->buffer, insert_range_cb, changed);
+          g_signal_handlers_disconnect_by_func (changed->buffer, delete_range_cb, changed);
+          g_signal_handlers_disconnect_by_func (changed->buffer, delete_range_after_cb, changed);
+          g_signal_handlers_disconnect_by_func (changed->buffer, mark_set_cb, changed);
+        }
+      g_clear_object (&changed->buffer);
+    }
+
+  g_object_set_data (G_OBJECT (widget), "accessible-text-data", NULL);
+}
index 2ee9b2d85f4b09293f04b0450f9975bf4d7e4488..14ebf8c2b43a2494c5feb4c843e7a1ae3bbd7581 100644 (file)
@@ -27,4 +27,19 @@ G_BEGIN_DECLS
 
 const GDBusInterfaceVTable *gtk_atspi_get_text_vtable (GtkWidget *widget);
 
+typedef void (GtkAtspiTextChangedCallback) (gpointer    data,
+                                            const char *kind,
+                                            int         start,
+                                            int         end,
+                                            const char *text);
+typedef void (GtkAtspiSelectionChangedCallback) (gpointer    data,
+                                                 const char *kind,
+                                                 int         position);
+
+void gtk_atspi_connect_text_signals    (GtkWidget *widget,
+                                        GtkAtspiTextChangedCallback text_changed,
+                                        GtkAtspiSelectionChangedCallback selection_changed,
+                                        gpointer   data);
+void gtk_atspi_disconnect_text_signals (GtkWidget *widget);
+
 G_END_DECLS